msg_tool\scripts\yuris\img/
ydg.rs1use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::img::*;
6use crate::utils::struct_pack::*;
7use anyhow::Result;
8use msg_tool_macro::*;
9use std::io::{Read, Seek, SeekFrom, Write};
10use std::sync::{Arc, Mutex};
11
12#[derive(StructPack, StructUnpack, Debug, Clone)]
13struct YDGHeader {
14 magic: [u8; 4],
16 yuris_magic: [u8; 8],
18 _unk: u32,
20 header_size: u32,
22 file_size: u32,
24 _unk1: u64,
25 width: u16,
27 height: u16,
29 #[pack_vec_len(self.header_size - 0x24)]
30 #[unpack_vec_len({
31 if header_size < 0x24 {
32 anyhow::bail!("Header size at least need 0x24 bytes.");
33 }
34 header_size - 0x24
35 })]
36 other: Vec<u8>,
37 #[pvec(u32)]
38 slices: Vec<Slice>,
39}
40
41#[derive(StructPack, StructUnpack, Debug, Clone)]
42struct Slice {
43 offset: u32,
45 size: u32,
47 x: u16,
48 height: u16,
49 _unk: u32,
50}
51
52#[derive(Debug)]
53pub struct YDGImageBuilder {}
55
56impl YDGImageBuilder {
57 pub fn new() -> Self {
58 Self {}
59 }
60}
61
62impl ScriptBuilder for YDGImageBuilder {
63 fn default_encoding(&self) -> Encoding {
64 Encoding::Utf8
65 }
66
67 fn build_script(
68 &self,
69 buf: Vec<u8>,
70 _filename: &str,
71 _encoding: Encoding,
72 _archive_encoding: Encoding,
73 config: &ExtraConfig,
74 _archive: Option<&Box<dyn Script>>,
75 ) -> Result<Box<dyn Script + Send + Sync>> {
76 Ok(Box::new(YDGImage::new(MemReader::new(buf), config)?))
77 }
78
79 fn extensions(&self) -> &'static [&'static str] {
80 &["ydg"]
81 }
82
83 fn script_type(&self) -> &'static ScriptType {
84 &ScriptType::YurisYDG
85 }
86
87 fn is_image(&self) -> bool {
88 true
89 }
90
91 fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
92 if buf_len >= 12 && buf.starts_with(b"YDG\0YU-RIS\0\0") {
93 return Some(50);
94 }
95 None
96 }
97
98 fn can_create_image_file(&self) -> bool {
99 true
100 }
101
102 fn create_image_file<'a>(
103 &'a self,
104 mut data: ImageData,
105 _filename: &str,
106 mut writer: Box<dyn WriteSeek + 'a>,
107 _options: &ExtraConfig,
108 ) -> Result<()> {
109 let mut header = YDGHeader {
110 magic: *b"YDG\0",
111 yuris_magic: *b"YU-RIS\0\0",
112 _unk: 0x64,
113 header_size: 0x30,
114 file_size: 0,
115 _unk1: 0,
116 width: data.width as u16,
117 height: data.height as u16,
118 other: vec![0; 0xC],
119 slices: vec![Slice {
120 offset: 0,
121 size: 0,
122 x: 0,
123 height: data.height as u16,
124 _unk: 0,
125 }],
126 };
127 header.pack(&mut writer, false, Encoding::Utf8, &None)?;
128 header.slices[0].offset = writer.stream_position()? as u32;
129 match data.color_type {
130 ImageColorType::Bgr => {
131 convert_bgr_to_rgb(&mut data)?;
132 }
133 ImageColorType::Bgra => {
134 convert_bgra_to_rgba(&mut data)?;
135 }
136 ImageColorType::Rgb | ImageColorType::Rgba => {}
137 ImageColorType::Grayscale => {
138 convert_grayscale_to_rgb(&mut data)?;
139 }
140 };
141 let encoder = qoi::Encoder::new(&data.data, data.width, data.height)?;
142 encoder.encode_to_stream(&mut writer)?;
143 let file_size = writer.stream_position()? as u32;
144 header.slices[0].size = file_size - header.slices[0].offset;
145 header.file_size = file_size;
146 writer.seek(SeekFrom::Start(0))?;
147 header.pack(&mut writer, false, Encoding::Utf8, &None)?;
148 Ok(())
149 }
150}
151
152#[derive(Debug)]
153pub struct YDGImage<T> {
154 inner: Arc<Mutex<T>>,
155 header: YDGHeader,
156}
157
158impl<T: Read + Seek> YDGImage<T> {
159 pub fn new(mut data: T, _config: &ExtraConfig) -> Result<Self> {
160 let header = YDGHeader::unpack(&mut data, false, Encoding::Utf8, &None)?;
161 if &header.magic != b"YDG\0" {
162 anyhow::bail!("Unknown YDG magic: {:?}", header.magic);
163 }
164 if &header.yuris_magic != b"YU-RIS\0\0" {
165 anyhow::bail!("Unknown YU-RIS magic: {:?}", header.yuris_magic);
166 }
167 Ok(Self {
168 inner: Arc::new(Mutex::new(data)),
169 header,
170 })
171 }
172
173 fn load_slice(&self, slice: &Slice) -> Result<ImageData> {
174 let mut data = StreamRegion::with_size(
175 MutexWrapper::new(self.inner.clone(), slice.offset as u64),
176 slice.size as u64,
177 )?;
178 let mut buf = [0; 12];
179 let readed = data.peek(&mut buf)?;
180 if readed == 12 && buf.starts_with(b"RIFF") && buf.ends_with(b"WEBP") {
181 load_webp(data)
182 } else {
183 load_qoi(data)
184 }
185 }
186}
187
188impl<T: Read + Seek + std::fmt::Debug> Script for YDGImage<T> {
189 fn default_output_script_type(&self) -> OutputScriptType {
190 OutputScriptType::Custom
191 }
192
193 fn is_output_supported(&self, output: OutputScriptType) -> bool {
194 matches!(output, OutputScriptType::Custom)
195 }
196
197 fn default_format_type(&self) -> FormatOptions {
198 FormatOptions::None
199 }
200
201 fn is_image(&self) -> bool {
202 true
203 }
204
205 fn export_image(&self) -> Result<ImageData> {
206 let slice = self
207 .header
208 .slices
209 .get(0)
210 .ok_or_else(|| anyhow::anyhow!("YDG image has no valid tiles."))?;
211 let mut y = 0;
212 let mut base = self.load_slice(slice)?;
213 convert_to_rgba(&mut base)?;
214 let mut base = draw_on_canvas(
215 base,
216 self.header.width as u32,
217 self.header.height as u32,
218 slice.x as u32,
219 y,
220 )?;
221 y += slice.height as u32;
222 for slice in &self.header.slices[1..] {
223 let mut diff = self.load_slice(slice)?;
224 convert_to_rgba(&mut diff)?;
225 draw_on_image(&mut base, &diff, slice.x as u32, y)?;
226 y += slice.height as u32;
227 }
228 Ok(base)
229 }
230
231 fn import_image<'a>(
232 &'a self,
233 mut data: ImageData,
234 _filename: &str,
235 mut file: Box<dyn WriteSeek + 'a>,
236 ) -> Result<()> {
237 if data.depth != 8 {
238 anyhow::bail!("Unsupported depth: {}", data.depth);
239 }
240 let mut header = self.header.clone();
241 header.slices.clear();
242 header.slices.push(Slice {
243 offset: 0,
244 size: 0,
245 x: 0,
246 height: data.height as u16,
247 _unk: 0,
248 });
249 header.pack(&mut file, false, Encoding::Utf8, &None)?;
250 header.slices[0].offset = file.stream_position()? as u32;
251 if header.width != data.width as u16 || header.height != data.height as u16 {
252 eprintln!(
253 "WARNING: image size dismatched, expected {}x{}, actually {}x{}.",
254 header.width, header.height, data.width, data.height
255 );
256 crate::COUNTER.inc_warning();
257 header.width = data.width as u16;
258 header.height = data.height as u16;
259 }
260 match data.color_type {
261 ImageColorType::Bgr => {
262 convert_bgr_to_rgb(&mut data)?;
263 }
264 ImageColorType::Bgra => {
265 convert_bgra_to_rgba(&mut data)?;
266 }
267 ImageColorType::Rgb | ImageColorType::Rgba => {}
268 ImageColorType::Grayscale => {
269 convert_grayscale_to_rgb(&mut data)?;
270 }
271 };
272 let encoder = qoi::Encoder::new(&data.data, data.width, data.height)?;
273 encoder.encode_to_stream(&mut file)?;
274 let file_size = file.stream_position()? as u32;
275 header.slices[0].size = file_size - header.slices[0].offset;
276 header.file_size = file_size;
277 file.seek(SeekFrom::Start(0))?;
278 header.pack(&mut file, false, Encoding::Utf8, &None)?;
279 Ok(())
280 }
281}